Skip to main content

TMBatching

Batching groups several writes so their listeners fire once, against the net change, instead of once per write. Under the hood a batch is deferred flushing: it holds off the diff→fire→reconcile cycle until the window closes (see the Flushing guide for what a flush is).

Why batch

Firing listeners after every individual write is both wasteful and can expose half-finished state to observers. Batch defers all firing until the whole group is done, then fires once for the combined diff.

manager:Batch(function()
	manager:Set("Player.Health", 100)
	manager:Set("Player.Mana", 50)
	manager:Set("Player.Stamina", 75)
end)
-- listeners fire here, once, for the combined diff

Net no-op changes within the window produce no events: if a value is changed and then changed back before the batch closes, nobody hears about it.

Suspend / Resume

Suspend() and Resume() are the manual form of Batch — use them when the writes can't be wrapped in a single function (e.g. they span a loop or several call sites). Nested Batch/Suspend calls are no-ops; the outermost one wins.

manager:Suspend()
for _, entry in pendingEdits do
	manager:Set(entry.path, entry.value)
end
manager:Resume() -- one flush: accumulated changes diffed against pre-suspend state
Do not yield inside a batch

Yielding (task.wait, awaiting a Promise, etc.) between Suspend and Resume — or inside a Batch callback — is unsupported. Keep the window synchronous.

What Resume fires

On resume the accumulated changes are diffed against the pre-batch state in a single array-aware pass, so array edits still surface as faithful ArrayInserted/ArrayRemoved/ArraySet events rather than a blanket "the array changed". A batched root-level write still reaches root ({}) listeners.

Batching vs. coalesced flushing

Both collapse many writes into one flush, but they answer to different things:

  • Batching is explicit — you bracket the writes with Batch/Suspend, and the flush happens the moment the window closes (synchronously).
  • Coalesced flushing (FlushMode = "coalesced") is automatic and frame-based — the manager merges the frame's flushes and fires them at frame end, with no bracketing on your part. See the Flushing guide.

See also

Show raw api
{
    "functions": [],
    "properties": [],
    "types": [],
    "name": "TM Batching",
    "desc": "Batching groups several writes so their listeners fire **once**, against the net\nchange, instead of once per write. Under the hood a batch is deferred flushing:\nit holds off the diff→fire→reconcile cycle until the window closes (see the\nFlushing guide for what a flush is).\n\n## Why batch\n\nFiring listeners after every individual write is both wasteful and can expose\nhalf-finished state to observers. `Batch` defers all firing until the whole\ngroup is done, then fires once for the combined diff.\n\n```lua\nmanager:Batch(function()\n\tmanager:Set(\"Player.Health\", 100)\n\tmanager:Set(\"Player.Mana\", 50)\n\tmanager:Set(\"Player.Stamina\", 75)\nend)\n-- listeners fire here, once, for the combined diff\n```\n\nNet no-op changes within the window produce no events: if a value is changed and\nthen changed back before the batch closes, nobody hears about it.\n\n## Suspend / Resume\n\n`Suspend()` and `Resume()` are the manual form of `Batch` — use them when the\nwrites can't be wrapped in a single function (e.g. they span a loop or several\ncall sites). Nested `Batch`/`Suspend` calls are no-ops; the outermost one wins.\n\n```lua\nmanager:Suspend()\nfor _, entry in pendingEdits do\n\tmanager:Set(entry.path, entry.value)\nend\nmanager:Resume() -- one flush: accumulated changes diffed against pre-suspend state\n```\n\n:::caution Do not yield inside a batch\nYielding (`task.wait`, awaiting a Promise, etc.) between `Suspend` and `Resume`\n— or inside a `Batch` callback — is unsupported. Keep the window synchronous.\n:::\n\n## What Resume fires\n\nOn resume the accumulated changes are diffed against the pre-batch state in a\nsingle array-aware pass, so array edits still surface as faithful\n`ArrayInserted`/`ArrayRemoved`/`ArraySet` events rather than a blanket \"the array\nchanged\". A batched root-level write still reaches root (`{}`) listeners.\n\n## Batching vs. coalesced flushing\n\nBoth collapse many writes into one flush, but they answer to different things:\n\n- **Batching** is explicit — *you* bracket the writes with `Batch`/`Suspend`, and\n  the flush happens the moment the window closes (synchronously).\n- **Coalesced flushing** (`FlushMode = \"coalesced\"`) is automatic and frame-based\n  — the manager merges the frame's flushes and fires them at frame end, with no\n  bracketing on your part. See the Flushing guide.\n\n---\n### See also\n\n- **[TM Flushing](/api/TM%20Flushing)** — the diff→fire→reconcile cycle a batch defers.\n- **[TM Listeners & Fire Modes](/api/TM%20Listeners%20&%20Fire%20Modes)** — what fires when the batch closes.\n- **[TM Getting Started](/api/TM%20Getting%20Started)** — the basics of reading and writing.",
    "source": {
        "line": 70,
        "path": "lib/tablemanager/src/Docs/TM_Batching.luau"
    }
}